package ca

import (
	"bytes"
	"encoding/json"
	"fmt"
	"gitee.com/wayjin/fabric-api/crypto"
	cfsslapi "github.com/cloudflare/cfssl/api"
	"github.com/mitchellh/mapstructure"
	"github.com/pkg/errors"
	"io/ioutil"
	"net/http"
)

var (
	ErrIdentityExist = errors.New("identity exist")
)

type Client struct {
	url string
	caname string
	admin  string
	adminpw string
	cli *http.Client
}

func NewClient(url, admin, adminpw string) *Client {
	return &Client{
		url: url,
		admin:admin,
		adminpw:adminpw,
		cli: &http.Client{},
	}
}

func (c *Client) URL(uri string) string {
	return fmt.Sprintf("%s/%s", c.url, uri)
}

func (c *Client) send(req *http.Request, result interface{}) error {
	if c.cli == nil {
		c.cli = &http.Client{}
	}
	resp, err := c.cli.Do(req)
	if err != nil {
		return fmt.Errorf("client request fail: %v", err)
	}

	defer resp.Body.Close()
	bodyData, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var res *cfsslapi.Response
	if bodyData != nil && len(bodyData) > 0 {
		res = new(cfsslapi.Response)
		err = json.Unmarshal(bodyData, res)
		if err != nil {
			return err
		}
		if len(res.Errors) > 0 {
			var errorMsg string
			for _, err1 := range res.Errors {
				msg := fmt.Sprintf("Response from server: Error Code: %d - %s\n", err1.Code, err1.Message)
				if errorMsg == "" {
					errorMsg = msg
				} else {
					errorMsg = errorMsg + fmt.Sprintf("\n%s", msg)
				}
			}
			return errors.Errorf(errorMsg)
		}
	}
	if resp.StatusCode >= 400 {
		return fmt.Errorf("invalid request code: %d", resp.StatusCode)
	}

	if res == nil {
		return fmt.Errorf("nil response result")
	}

	if result != nil {
		return mapstructure.Decode(res.Result, result)
	}
	return nil
}

// Enroll: get crypto certificate from server
func (c *Client) Enroll(req EnrollmentRequest) (KeyStore, error) {
	if req.CSR == nil {
		req.CSR = &crypto.CSRInfo{
			CN:    req.Name,
			Hosts: []string{req.Name},
		}
	}

	certPEM, key, err := crypto.GenerateKey(req.CSR, req.Name)
	if err != nil {
		return nil, err
	}

	reqNet := EnrollmentRequestNet{CAName: req.CAName}
	reqNet.SignRequest.Request = string(certPEM)
	if req.Profile != "" {
		reqNet.Profile = req.Profile
	}
	reqNet.Label = req.Label
	body, err := Marshal(&reqNet, "SignRequest")
	if err != nil {
		return nil, err
	}
	request, err := http.NewRequest("POST", c.URL("enroll"), bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	request.SetBasicAuth(req.Name, req.Secret)
	var respNet EnrollmentResponseNet
	err = c.send(request, &respNet)
	if err != nil {
		return nil, err
	}

	signCert, err := crypto.B64Decode(respNet.Cert)
	if err != nil {
		return nil, err
	}
	caCert, err := crypto.B64Decode(respNet.ServerInfo.CAChain)
	if err != nil {
		return nil, err
	}
	cryptoCert := cryptoCert{
		signcert: signCert,
		key:      key,
		cacert:   caCert,
	}
	return cryptoCert, nil
}

// Register: register role
func (c *Client) Register(req RegistrationRequest, auth KeyStore) error {
	id := &identity{
		cert:   auth.GetSignCert(),
		key:    auth.GetKey(),
		client: c,
	}
	_, err := id.getIdentity(req.Name, req.CAName)
	if err == nil {
		return ErrIdentityExist
	}
	_, err = id.getAffiliation(req.Affiliation, req.CAName)
	if err != nil {
		err = id.addAffiliation(req.Affiliation, req.CAName, true)
		if err != nil {
			return fmt.Errorf("add affiliation %s fail: %v", req.Affiliation, err)
		}
	}
	_, err = id.addIdentity(req)
	return err
}

// Cancel: Cancellation of registration
func (c *Client) Cancel(name, caname string, auth KeyStore) error {
	id := &identity{
		cert:   auth.GetSignCert(),
		key:    auth.GetKey(),
		client: c,
	}
	_, err := id.getIdentity(name, caname)
	if err != nil {
		return err
	}
	return id.delIdentity(name, caname, true)
}

func (c *Client) RegisterAndEnroll(req RegistrationRequest, auth KeyStore) (KeyStore, error) {
	var err error
	if auth == nil {
		auth, err = c.Enroll(EnrollmentRequest{Name:c.admin, Secret:c.adminpw})
		if err != nil {
			return nil, err
		}
	}
	err = c.Register(req, auth)
	if err != nil && err != ErrIdentityExist {
		return nil, err
	}
	return c.Enroll(EnrollmentRequest{Name:req.Name, Secret:req.Secret, Profile:req.Profile})
}